/* Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
 
Copyright (C) 2010 by Tomasz Pluskiewicz

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. */

using System;
using System.Data;
using System.Reflection;
using Castle.Core.Configuration;
using Castle.MonoRail.Framework;
using Yama.MVC.Dao.Connections;
using Yama.MVC.MonoRail.Attributes;
using Yama.MVC.MonoRail.Utils;
using Yama.Util.CodeContracts;

namespace Yama.MVC.MonoRail.Extensions
{
    /// <summary>
    /// Extension class, which handles opening and closing transaction during a MonoRail request.
    ///
    /// It requires an implementation of <see cref="TransactionalActionHandler"/>.
    /// </summary>
    public class TransactionManagementExtension : IMonoRailExtension
    {
        TransactionalActionHandler _transactionHandler;
        ICacheProvider _cacheProvider;
        IsolationLevel _defaultIsolationLevel = IsolationLevel.Serializable;

        public static event EventHandler<TransactionEventArgs> CommitException;

        #region Implementation of IMRServiceEnabled

        public void Service(IMonoRailServices serviceProvider)
        {
            ExtensionManager manager = serviceProvider.GetService<ExtensionManager>();

            manager.PreControllerProcess += BeginTransaction;
            manager.PostControllerProcess += EndTransaction;
            manager.ActionException += RollbackTransaction;

            _cacheProvider = serviceProvider.GetService<ICacheProvider>();
        }

        public void SetExtensionConfigNode(IConfiguration node)
        {
            CreateTransactionHandler(node);
            SetDefaultIsolationLevel(node);
        }

        private void SetDefaultIsolationLevel(IConfiguration node)
        {
            IsolationLevel defaultIsolationLevel;
            try
            {
                defaultIsolationLevel = (IsolationLevel)Enum.Parse(typeof(IsolationLevel), node.Attributes["defaultIsolationLevel"]);
            }
            catch
            {
                defaultIsolationLevel = IsolationLevel.Serializable;
            }

            _defaultIsolationLevel = defaultIsolationLevel;
        }

        private void CreateTransactionHandler(IConfiguration node)
        {
            string typeName = node.Attributes["TransactionalActionHandlerType"];

            Check.Require(!string.IsNullOrEmpty(typeName), "Missing attribute TransactionalActionHandlerType for TransactionManagementExtension!");

            this._transactionHandler = Activator.CreateInstance(Type.GetType(typeName)) as TransactionalActionHandler;

            Check.Ensure(this._transactionHandler != null, string.Format("Could not create an instance of {0}", typeName));
        }

        #endregion

        /// <summary>
        /// Begin a transaction before action executes
        /// </summary>
        void BeginTransaction(IEngineContext context)
        {
            // Listen for the selected action to be intercepted
            SelectActionInterceptor.CurrentActionSelected += (sender, args) => BeginTransaction(args, context);

            // If SelectActionInterceptor did not fire, start a transaction in the
            // IController BeforeAction event (after filters!)
            context.CurrentController.BeforeAction += BeginTransaction;
        }

        void BeginTransaction(ActionSelectedDelegateArgs args, IEngineContext context)
        {
            MethodInfo executingMethod = ActionsUtil.GetMethodFromExecutableAction(args.SelectedAction, context.CurrentControllerContext);

            BeginTransaction(executingMethod);
        }

        void BeginTransaction(IExecutableAction action, IEngineContext context, IController controller, IControllerContext controllerContext)
        {
            // skip if SelectActionInterceptor has captured the action and a transaction is already open
            if (this._transactionHandler.IsTransactionActive) 
                return;

            MethodInfo executingMethod = ActionsUtil.GetMethodFromExecutableAction(action, controllerContext);

            BeginTransaction(executingMethod);
        }

        /// <summary>
        /// Check executing action-method for attributes and open transaction if necessary
        /// </summary>
        void BeginTransaction(MethodInfo executingMethod)
        {
            if (executingMethod == null || executingMethod.IsDefined(typeof(SkipTransactionAttribute), true))
                return;

            TransactionalActionAttribute attribute = GetAttributeForExecutingAction(executingMethod);

            IsolationLevel isolationLevel = attribute == null
                                                ? _defaultIsolationLevel
                                                : attribute.IsolationLevel;

            this._transactionHandler.BeginTransaction(isolationLevel);
        }

        void EndTransaction(IEngineContext context)
        {
            try
            {
                if (this._transactionHandler.IsTransactionActive)
                {
                    if (context.LastException != null)
                    {
                        this._transactionHandler.RollbackTransaction();
                    }
                    else
                    {
                        this._transactionHandler.CommitTransaction();
                    }
                }
            }
            catch (Exception ex)
            {
                this._transactionHandler.RollbackTransaction();

                if (CommitException != null)
                {
                    var eventArgs = new TransactionEventArgs(ex);
                    CommitException.Invoke(this, eventArgs);

                    if (!eventArgs.Handled)
                        throw new ApplicationException("An error occured at transaction commit. This must not be!", ex);
                }
            }
            finally
            {
                this._transactionHandler.DisposeOfTransaction();
            }
        }

        void RollbackTransaction(IEngineContext context)
        {
            if (this._transactionHandler.IsTransactionActive)
            {
                this._transactionHandler.RollbackTransaction();
                this._transactionHandler.DisposeOfTransaction();
            }
        }

        TransactionalActionAttribute GetAttributeForExecutingAction(MethodInfo currentMethod)
        {
            string cacheKey = string.Format("Transaction filters {0} {1}", currentMethod.Module.FullyQualifiedName, currentMethod.MetadataToken);

            if (!_cacheProvider.HasKey(cacheKey))
            {
                TransactionalActionAttribute attribute = ExtractAttributeFromMethod(currentMethod);

                if (attribute == null)
                    return null;

                _cacheProvider.Store(cacheKey, attribute);
            }

            return (TransactionalActionAttribute)_cacheProvider.Get(cacheKey);
        }

        static TransactionalActionAttribute ExtractAttributeFromMethod(MethodInfo method)
        {
            if (method == null) throw new ArgumentNullException("method");

            object[] attributes = method.GetCustomAttributes(typeof(TransactionalActionAttribute), true);

            Check.Invariant(attributes.Length <= 1, string.Format("Action can only have one TransactionalActionAttribute! Actual amount was {0}", attributes.Length));

            return (TransactionalActionAttribute)(attributes.Length == 0 ? null : attributes[0]);
        }
    }

    public class TransactionEventArgs : EventArgs
    {
        internal TransactionEventArgs(Exception exception)
        {
            Exception = exception;
            Handled = false;
        }

        public Exception Exception { get; set; }

        public bool Handled { get; set; }
    }
}